/***************************************************************************************************/
/*!
 @file	AudioFileWriter.m
 @discussion ￼
 Ported from AIFFWriter.m (Apple Software)
 Modified By: Hiroki Omae
 Original Author: QuickTime DTS
 
 Change History (most recent first): 
 
 <2> 03/24/06 must pass NSError objects to exportCompleted
 <1> 11/10/05 initial release
 
 � Copyright 2005-2006 Apple Computer, Inc. All rights reserved. 
 
 @discussion
This program is free software; you can redistribute it and/or modify
 it under the terms of the GNU General Public License as published by
 the Free Software Foundation; either version 2 of the License, or
 (at your option) any later version.
 
 This program is distributed in the hope that it will be useful,
 but WITHOUT ANY WARRANTY; without even the implied warranty of
 MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 GNU General Public License for more details.
 
 You should have received a copy of the GNU General Public License
 along with this program; if not, write to the Free Software
 Foundation, Inc., 59 Temple Place, Suite 330, Boston, MA  02111-1307  USA  
 */
/***************************************************************************************************/

//------------------------------------------------------------------------------------------------------------------------------------------------
// File Dependencies
//------------------------------------------------------------------------------------------------------------------------------------------------
#import "AudioFileWriter.h"

//------------------------------------------------------------------------------------------------------------------------------------------------
// Local Defines
//------------------------------------------------------------------------------------------------------------------------------------------------
// maximum size in frames of the MovieAudioExtractionFillBuffer calls
#define kMaxExtractionPacketCount 4096

//------------------------------------------------------------------------------------------------------------------------------------------------
// AudioFileWriter private def.
//------------------------------------------------------------------------------------------------------------------------------------------------
#pragma mark ---- AudioFileWriter private interface ----

@interface AudioFileWriter (Private)

- (OSStatus)extractAudioToFile:(SInt64 *)ioNumSamples;
- (OSStatus)getDefaultExtractionInfo;
- (OSStatus)configureExtractionSessionWithMovie:(Movie)inMovie;

- (OSStatus)exportMovie;
- (void)setMovieExtractionDuration;
- (void)exportCompletedNotification:(NSError *)inError;

@end


//------------------------------------------------------------------------------------------------------------------------------------------------
// AudioFileWriter impl.
//------------------------------------------------------------------------------------------------------------------------------------------------
@implementation AudioFileWriter

#pragma mark ---- initialization/dealocation ----

/*!
 @method    
 @abstract   Destructor
 */
- (void)dealloc
{
	if (mFileName) {
		[mFileName release];
	}
	
	if (mAudioExtractionSession){
		MovieAudioExtractionEnd(mAudioExtractionSession);
	}
	
	if (mQTMovie) {
		[mQTMovie release];
	}
	
	if (mExtractionLayoutPtr) {
		free(mExtractionLayoutPtr);
	}
	
	[super dealloc]; 
}

#pragma mark ---- public ----

/*!
 @method    
 @abstract   Exports audio file from QTMovie(Facade Method)
 @discussion
 main method call that will produce an AIFF file - it will try to
 export the movie on a separate thread but if it can't will schedule
 callbacks on the main thread 
 */
- (OSStatus)exportAudioFileFromMovie:(QTMovie *)inMovie toFile:(NSString *)inFullPath error:(NSError**)ppError 
{
	NSString *directory;	
	OSStatus err = noErr;
	
	// sanity
	if (nil == inMovie || nil == inFullPath) {
		return paramErr;
	}
	
	directory = [inFullPath stringByDeletingLastPathComponent];
	mFileName = [[NSString alloc] initWithString:[inFullPath lastPathComponent]];
	
	// retain the QTMovie object passed in, we need it for the duration of
	// the export regardless of what the client decides to do with it
	mQTMovie = [inMovie retain];
	
	// if the file already exists, delete it
	err = FSPathMakeRef((const UInt8*)[inFullPath fileSystemRepresentation], &mFileRef, false);
	if (err == noErr) {
		err = FSDeleteObject(&mFileRef);
		if (err) goto bail;
	}
	
	err = FSPathMakeRef((const UInt8*)[directory fileSystemRepresentation], &mParentRef, NULL);
	if (err) goto bail;
	
	// set the movies extraction duration in floating-point seconds
	[self setMovieExtractionDuration];
	err = [self exportMovie];
bail:
	if (err) {
		[self exportCompletedNotification:[NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]];
	}
	
	return err;
}

#pragma mark ---- private ----
/*!
 @method    
 @abstract   Export audio from movie.
 @discussion
 Actual method to do extraction. This method blocks during whole extraction process.
 */
-(OSStatus)exportMovie 
{
	OSStatus err = noErr;
	
	// prepare for extraction if this is the first entry
	if (NULL == mAudioExtractionSession) {
		
		err = [self configureExtractionSessionWithMovie: [mQTMovie quickTimeMovie]];
		if (err) goto bail;
	}
	
	// create the file
	if (0 == mExportFileID) {
		err = AudioFileCreate(&mParentRef, (CFStringRef)mFileName, kAudioFileAIFFType, &mOutputASBD, 0, &mFileRef, &mExportFileID);
		if (err) goto bail;
		
		// set the channel labels we grabbed from the source
		if (NULL != mExtractionLayoutPtr) {
			err = AudioFileSetProperty(mExportFileID, 
									   kAudioFilePropertyChannelLayout,
									   mExtractionLayoutSize,
									   (void *)mExtractionLayoutPtr);
			if (err) goto bail;
		}
	}
	
	// on entry if there's no samples left we're done
	if (mSamplesRemaining == 0) {
		mExtractionComplete = YES;
	}
	
	// perform full extraction
	while (!mExtractionComplete) {
		
		// read numSamplesThisSlice number of samples
		SInt64 numSamplesThisSlice = mSamplesRemaining;
		
		if ((numSamplesThisSlice > kMaxExtractionPacketCount) || (numSamplesThisSlice == -1)) {
			numSamplesThisSlice = kMaxExtractionPacketCount;
		}
		
		// extract the audio and write it to the file
		err = [self extractAudioToFile:&numSamplesThisSlice];	
		if (err) {
			goto bail;
		}
		if (mSamplesRemaining != -1) {
			mSamplesRemaining -= numSamplesThisSlice;
			mSamplesCompleated += numSamplesThisSlice;
			
			if (mSamplesRemaining == 0) {
				mExtractionComplete = YES;
			}
		}
	}
	
bail:
	// we're done either way so close the file
	if (mExportFileID) {
		AudioFileClose(mExportFileID);
	}
	if (err && mExportFileID) {
		// if we erred out, delete the file
		FSDeleteObject(&mFileRef);
		mExportFileID = 0;
	} 
	
	// call the completion routine to clean up
	[self exportCompletedNotification:[NSError errorWithDomain:NSOSStatusErrorDomain code:err userInfo:nil]];
	
	return err;
}


/*!
 @method     
 @abstract   Extract Audio to file.
 @discussion 
 extract a slice of PCM audio and write it to an AIFF file - audio extraction proceeds serially
 from the last position, 'ploc' specifies the file offset that this buffer should be written to
 could be optimized by supplying a buffer, but for now it is simply allocated and released in each call
 */
- (OSStatus)extractAudioToFile:(SInt64 *)ioNumSamples
{
	AudioBufferList	bufList;		
	UInt32			bufsize;
	char			*buffer = NULL;
	UInt32			flags;
	UInt32			numFrames;
	UInt32			bytesPerFrame;
	
	OSStatus		err;
	
	numFrames = *ioNumSamples;
	
	if( mOutputASBD.mBytesPerFrame ) {
		bytesPerFrame = mOutputASBD.mBytesPerFrame;
	} else {
		UInt32		size = sizeof(UInt32);
		UInt32		maxPacketSize;
		err = AudioFileGetProperty(mExportFileID, kAudioFilePropertyPacketSizeUpperBound, &size, &maxPacketSize);
		if( err ) {
			goto bail;
		}
		bytesPerFrame = maxPacketSize / mOutputASBD.mFramesPerPacket;
	}
	
	bufsize = (numFrames * bytesPerFrame);
	buffer = (char *)malloc(bufsize);
	if (NULL == buffer) {
		err = memFullErr;
		goto bail;
	}
	
	// always extract interleaved data, since that's all we can write to an AIFF file
	bufList.mNumberBuffers = 1;
	bufList.mBuffers[0].mNumberChannels = mOutputASBD.mChannelsPerFrame;
	bufList.mBuffers[0].mDataByteSize = bufsize;
	bufList.mBuffers[0].mData = buffer;
	
	// read the number of requested samples from the movie
	err = MovieAudioExtractionFillBuffer(mAudioExtractionSession, &numFrames, &bufList, &flags);
	if (err) goto bail;
	
	// write it to the file
	if (numFrames > 0) {
		err = AudioFileWritePackets(mExportFileID,
									false,
									numFrames * mOutputASBD.mBytesPerPacket,
									NULL,
									mLocationInFile,
									&numFrames,
									buffer);
		if (err) goto bail;							
		
		mLocationInFile += numFrames;
	}
	
bail:
	if (NULL != buffer) { 
		free(buffer);
	}
	if (err) {
		numFrames = 0;
	}
	
	*ioNumSamples = numFrames;
	mExtractionComplete = (flags & kQTMovieAudioExtractionComplete);	
	return err;
}

/*!
 @method    
 @abstract   Get default extraction layout
 @discussion
 get the default extraction layout for this movie, expanded into individual channel descriptions
 
 @discussion
 The channel layout returned by this routine must be deallocated by the client
 If 'asbd' is non-NULL, fill it with the default extraction asbd, which contains the
 highest sample rate among the sound tracks that will be contributing.
 'outLayoutSize' and 'asbd' may be nil.
 */
- (OSStatus)getDefaultExtractionInfo
{
	OSStatus err;
	
	// get the size of the extraction output layout
	err = MovieAudioExtractionGetPropertyInfo(mAudioExtractionSession,
											  kQTPropertyClass_MovieAudioExtraction_Audio,
											  kQTMovieAudioExtractionAudioPropertyID_AudioChannelLayout,
											  NULL,
											  &mExtractionLayoutSize,
											  NULL);
	if (err) goto bail;
	
	// allocate memory for the layout
	mExtractionLayoutPtr = (AudioChannelLayout *)calloc(1, mExtractionLayoutSize);
	if (NULL == mExtractionLayoutPtr)  { err = memFullErr; goto bail; }
	
	// get the layout for the current extraction configuration
	err = MovieAudioExtractionGetProperty(mAudioExtractionSession,
										  kQTPropertyClass_MovieAudioExtraction_Audio,
										  kQTMovieAudioExtractionAudioPropertyID_AudioChannelLayout,
										  mExtractionLayoutSize,
										  mExtractionLayoutPtr,
										  NULL);
	if (err) {
		goto bail;
	}
	// get the audio stream basic description
	err = MovieAudioExtractionGetProperty(mAudioExtractionSession,
										  kQTPropertyClass_MovieAudioExtraction_Audio,
										  kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription,
										  sizeof(AudioStreamBasicDescription),
										  &mSourceASBD,
										  NULL);    
	
bail:
	return err;				
}

/*!
 @method    
 @abstract   Sets up extraction settings
 @discussion
 This method prepare the specified movie for extraction by opening an extraction session, configuring
 and setting the output ASBD and the output layout if one exists - it also sets the start time to 0
 and calculates the total number of samples to export
 */
- (OSStatus) configureExtractionSessionWithMovie:(Movie)inMovie
{
	OSStatus err;
	
	// open a movie audio extraction session
	err = MovieAudioExtractionBegin(inMovie, 0, &mAudioExtractionSession);
	if (err) goto bail;
	
	err = [self getDefaultExtractionInfo];
	if (err) goto bail;
	
	/*
	 Setting up extraction format.
	 
	 set the output ASBD to 16-bit interleaved PCM big-endian integers
	 we start with the default ASBD which has set the sample rate to the
	 highest rate among all audio tracks 
	 */
	mOutputASBD = mSourceASBD;
	
	mOutputASBD.mFormatID 			= kAudioFormatLinearPCM;
	mOutputASBD.mFormatFlags 		= kLinearPCMFormatFlagIsSignedInteger |
	kAudioFormatFlagIsBigEndian |
	kAudioFormatFlagIsPacked;
	mOutputASBD.mFramesPerPacket 	= 1;
	mOutputASBD.mBitsPerChannel 		= 16;
	mOutputASBD.mBytesPerFrame 		= 2 * mOutputASBD.mChannelsPerFrame;
	mOutputASBD.mBytesPerPacket 		= 2 * mOutputASBD.mChannelsPerFrame;
	
	// set the extraction ASBD
	err = MovieAudioExtractionSetProperty(mAudioExtractionSession,
										  kQTPropertyClass_MovieAudioExtraction_Audio,
										  kQTMovieAudioExtractionAudioPropertyID_AudioStreamBasicDescription,
										  sizeof(mOutputASBD),
										  &mOutputASBD);
	if (err) {
		goto bail;		
	}
	
	// set the output layout
	if (mExtractionLayoutPtr) {
		err = MovieAudioExtractionSetProperty(mAudioExtractionSession,
											  kQTPropertyClass_MovieAudioExtraction_Audio,
											  kQTMovieAudioExtractionAudioPropertyID_AudioChannelLayout,
											  mExtractionLayoutSize,
											  mExtractionLayoutPtr);
		if (err) {
			goto bail;
		}
	}
	
	// set the extraction start time - we always start at zero, but you don't have to
	TimeRecord startTime = { 0, 0, GetMovieTimeScale(inMovie), GetMovieTimeBase(inMovie) };
	
	err = MovieAudioExtractionSetProperty(mAudioExtractionSession,
										  kQTPropertyClass_MovieAudioExtraction_Movie,
										  kQTMovieAudioExtractionMoviePropertyID_CurrentTime,
										  sizeof(TimeRecord), &startTime);
	if (err) {
		goto bail;
	}
	
	// set the number of total samples to export
	mSamplesRemaining = mMovieDuration ? (mMovieDuration * mOutputASBD.mSampleRate) : -1;
	mTotalNumberOfSamples = mSamplesRemaining;
bail:	
	return err;
}


/*!
 @method    
 @abstract   Calcurate exporting movie duration (in Sec.)
 @discussion
 calculate the duration of the longest audio track in the movie
 if the audio tracks end at time N and the movie is much 
 longer we don't want to keep extracting - the API will happily
 return zeroes until it reaches the movie duration
 */
-(void)setMovieExtractionDuration
{
	TimeValue maxDuration = 0;
	UInt8 i;
	
	SInt32 trackCount = GetMovieTrackCount([mQTMovie quickTimeMovie]);
	
	if (trackCount) {
		for (i = 1; i < trackCount + 1; i++) {
			Track aTrack = GetMovieIndTrackType([mQTMovie quickTimeMovie],
												i,
												SoundMediaType,
												movieTrackMediaType);
			if (aTrack) {
				TimeValue aDuration = GetTrackDuration(aTrack);
				
				if (aDuration > maxDuration) maxDuration = aDuration;
			}
		}
		
		mMovieDuration = (Float64)maxDuration / (Float64)GetMovieTimeScale([mQTMovie quickTimeMovie]);
	}
}


/*!
 @method    
 @abstract   Cleanup resources and prepare for reuse.
 @discussion
 this completion method gets called at the end of the extraction session or may be called
 earlier if an error has occured - its main purpose is to clean up the world so an AudioFileWriter
 instance can be used over and over again
 
 in this particular case if we completed successfully we launch QTPlayer with the .aif file
 and if an error occurs we pass it back to the client though the progress info object
 */
- (void) exportCompletedNotification:(NSError *)inError
{
	if (mFileName) {
		[mFileName release];
		mFileName = nil;
	}
	
	if (mAudioExtractionSession){
		MovieAudioExtractionEnd(mAudioExtractionSession);
		mAudioExtractionSession = NULL;
	}
	
	if (mQTMovie) {
		[mQTMovie release];
		mQTMovie = nil;
	}
	
	mMovieDuration 		= 0;
	mExtractionComplete 	= NO;
	mLocationInFile 		= 0;
	mSamplesRemaining 	= 0;
	mSamplesCompleated 	= 0;
	mTotalNumberOfSamples 	= 0;
	
	if (mExtractionLayoutPtr) {
		free(mExtractionLayoutPtr);
		mExtractionLayoutPtr = NULL;
	}
	
	mExtractionLayoutSize 	= 0;
	mExportFileID 		= 0;
}

@end
